/* ********************************************************************************************
*  Alvin Baldemeca
*  Tetris Project
*  Course: TCSS 305 , Alan Fowler
*  Term: Spring 2012
*  UNIVERISTY OF WASHINGTON TACOMA
**********************************************************************************************/

package controller;

import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Random;

import model.AbstractPiece;
import model.IPiece;
import model.JPiece;
import model.LPiece;
import model.OPiece;
import model.Piece;
import model.SPiece;
import model.TPiece;
import model.ZPiece;

/**
 * This class represents a Tetris board that contains Tetris pieces.
 * This is a model and controller used to control the Tetris piece.
 * @author Alvin Baldemeca
 * @version 5/3/2012
 */
public class Board extends Observable 
{
  /**
   * The maximum rotation possible for any piece.
   */
  private static final int NUMBER_OF_TEST_PIECES = 10;
  /**
   * The maximum rotation possible for any piece.
   */
  private static final int MAX_ROTATION = 4;
  
  /**
   * A Random that is used to select a random tetris piece.
   */
  private static final Random RANDOM = new Random();
  
  /**
   * The initial y coordinate of a starting piece.
   */
  private static final int STARTING_HEIGHT = -1;
  
  /**
   * The base score given when a piece is created.
   */
  private static final int BASE_SCORE = 10;
  
  /**
   * The minimum number of undo pieces that's needed to be left in the array.
   */
  private static final int MINIUM_NUMBER_OF_UNDO_PIECES = 3;
  
  /**
   * The index of the sequence that will determine the next piece in the
   * sequence.
   */
  private int my_seq;
  
 /**     
  * The height of the board.
  */
  private final int my_height;
  
 
  /**
   * The width of the board.
   */
  private final int my_width;
  
  /**
   * The board represented in 2D integer array.
   */
  private Color [][] my_container;
  
  /**
   * The list of boards state each time a tetris piece is created.
   */
  private List<Color[][]> my_container_list;
  
  /**
   * The current piece on the board.
   */
  private AbstractPiece my_current_piece;
  
  
  /**
   * The current piece on the board.
   */
  private AbstractPiece my_next_piece;
  
  /**
   * The tetris piece to swap.
   */
  private AbstractPiece my_swap_piece;
  
  /**
   * States wether a piece can be swapped.
   */
  private boolean my_can_swap;
    
  /**
   * The list of all the pieces dropped which can be undo.
   */
  private final List<AbstractPiece> my_undo_piece_list;
  
  /**
   * Whether the creating of a piece will be random or not.
   */
  private boolean my_random;
  
  /**
   * The status of the game.  States true when the game is over otherwise its false.
   */
  private boolean my_game_over_status;
  
  /**
   * The score on the tetris board.
   */
  private int my_score;
  
  /**
   * The number of lines cleared on the tetris board.
   */
  private int my_number_of_lines_cleared;
  
  /**
   * The different kinds of Tetris piece that can be selected when creating
   * a new game piece.
   * @author Alvin Baldemeca
   * @version 5/5/2012
   */
  private enum Tetris 
  {
    /**
     * A tetris I-piece.
     */
    IPIECE, 
    
    /**
     * A tetris J-piece.
     */
    JPIECE, 
    
    /**
     * A tetris L-piece.
     */
    LPIECE, 
    
    /**
     * A tetris O-piece.
     */
    OPIECE, 
    
    /**
     * A tetris S-piece.
     */
    SPIECE, 
    
    /**
     * A tetris T-piece.
     */
    TPIECE, 
    
    /**
     * A tetris Z-piece.
     */
    ZPIECE 
    
  }
  
  /**
   * States whether the current piece is frozen.
   */
  private boolean my_piece_frozen;
  
  /**
   * Constructs a board with a given height and width.
   * @param the_height the height to construct the board
   * @param the_width the width to construct the board
   */
  public Board(final int the_height, final int the_width)
  {
    super();
    my_height = the_height;
    my_width = the_width;
    my_container = new Color[the_height][the_width]; 
    my_seq = 0;
    my_piece_frozen = false;
    my_game_over_status = false;
    my_container_list = new ArrayList<Color[][]>();
    my_undo_piece_list = new ArrayList<AbstractPiece>();
    initializeBoard();
  }
  
  /**
   * This method helps the constructor in initializing the board so that it will
   * select the mode either random or sequential and creat 2 tetris piece: the 
   * current piece and the next piece.
   */
  private void initializeBoard()
  {
    my_number_of_lines_cleared = 0;
    my_swap_piece = new OPiece();
    my_can_swap = true;
    setRandom(true);
    createPiece();
    my_score = my_score - BASE_SCORE;
    createPiece();
    my_score = my_score - BASE_SCORE;
  }
  
  /**
   * Creates an new Tetris piece.
   */
  public final void createPiece() 
  { 
    if (my_game_over_status)
    {
      my_container_list = new ArrayList<Color[][]>();
    }
   
    else
    {      
      my_score = my_score + BASE_SCORE; 
      my_piece_frozen = false;
      my_can_swap = true;
      if (my_random)
      {
        getRandomPiece();    
      }
      else
      { 
        getSequentialPiece();
      }
      
      my_undo_piece_list.add(getCopyPiece(my_current_piece));
      my_container_list.add(getContainer());
      setChanged(); 
      notifyObservers();
    }

    
  }
  
  /**
   * Creates a random Tetris piece.
   */
  private void getRandomPiece()
  {
    
    my_current_piece = my_next_piece;
    final int middle = my_width / 2 - 1; 
    final Tetris piece = Tetris.values()[RANDOM.nextInt(Tetris.values().length)]; 
    switch(piece) 
    {
      case IPIECE : 
        my_next_piece = new SPiece(middle, STARTING_HEIGHT);
        my_next_piece.setColor(Color.RED);
        break;
      
      case JPIECE :
        my_next_piece = new LPiece(middle, STARTING_HEIGHT);
        my_next_piece.setColor(Color.ORANGE);
        break;
      case LPIECE :
        my_next_piece = new OPiece(middle, STARTING_HEIGHT);
 
        my_next_piece.setColor(Color.YELLOW);
        break; 
      case OPIECE :
        my_next_piece = new JPiece(middle, STARTING_HEIGHT);
      
        my_next_piece.setColor(Color.GREEN);
        break; 
      case SPIECE :
        my_next_piece = new ZPiece(middle, STARTING_HEIGHT);
   
        my_next_piece.setColor(Color.BLUE);
        break;  
      case TPIECE :
        my_next_piece = new TPiece(middle, STARTING_HEIGHT);
        my_next_piece.setColor(Color.CYAN);
        break; 
      case ZPIECE :
        my_next_piece = new IPiece(middle, STARTING_HEIGHT);
     
        my_next_piece.setColor(Color.MAGENTA);
        break;
      default :
        my_next_piece = new OPiece(middle, STARTING_HEIGHT);
        my_next_piece.setColor(Color.YELLOW);
        break;
    }
   
  }
  
  /**
   * Creates a sequential tetris piece for testing.
   */
  private void getSequentialPiece()
  { 
    final AbstractPiece[] pclone = new AbstractPiece[] 
    { 
      new TPiece(-1, -1, 1),
      new TPiece(2, -1, 3),
      new IPiece(0, -1),
      new IPiece(3, -1, 1), 
      new OPiece(5, -1),
      new OPiece(5, -1),
      new OPiece(7, -1),
      new OPiece(7, -1),
      new IPiece(8, -1, 1),  //
      new OPiece(1, -1)
    };
   
    my_current_piece =  pclone[my_seq];
    my_current_piece.setColor(Color.RED);
    my_seq = (my_seq + 1) % NUMBER_OF_TEST_PIECES;

  }
 
  /**
   * Gets the current piece on the board.
   * @return my_current_piece on the board
   */
  public Piece getCurrentPiece()
  {  
    return my_current_piece;
  }
  
  /**
   * Returns the next piece that is waiting to be dropped on the board.
   * @return the next piece to be played on the board when the current piece freezes.
   */
  public Piece getNextPiece()
  {
    return my_next_piece;
  }
  
  /**
   * Gets the swapped piece.
   * @return the swapped piece
   */
  public AbstractPiece getSwapPiece()
  {
    return my_swap_piece;
  }
  
  /**
   * Sets what the swapped piece should be.
   * @param the_piece the swapped piece.
   */
  public void setSwapPiece(final AbstractPiece the_piece)
  {
    my_swap_piece = the_piece;
  }
  
  /**
   * Swaps the current piece with the saved swapped piece.
   */
  public void swapPiece()
  {
    
    if (my_swap_piece == null)
    {
      my_swap_piece = my_current_piece;
      createPiece();
      my_can_swap = false;
      setChanged();
      notifyObservers();
    }
    if (my_can_swap)
    {
      final AbstractPiece nextpiece = getCopyPiece(my_current_piece);
      my_current_piece = getCopyPiece(my_swap_piece);
      my_swap_piece = nextpiece;
      my_can_swap = false;
      setChanged();
      notifyObservers();
    }
      
  }
  
  /**
   * Undo the previews piece and assigns the current piece back to the undo piece.
   */
  public void undoPiece()
  {
    if (my_current_piece.getY() <= 1)
    {
      if (my_undo_piece_list.size() > MINIUM_NUMBER_OF_UNDO_PIECES)
      {
        final int index = my_undo_piece_list.size() - 1;
        my_current_piece = getCopyPiece(my_undo_piece_list.get(index - 1));   
        my_next_piece = getCopyPiece(my_undo_piece_list.get(index));
        my_undo_piece_list.remove(index);
        my_container_list.remove(my_container_list.size() - 1);
        my_container = getContainer(my_container_list.get(my_container_list.size() - 1));
        my_score = my_score - BASE_SCORE;
      }
      
    }
    else
    {
      my_current_piece = getCopyPiece(my_current_piece);
    }
  }
  
  /**
   * Creats a copy of an AbstractPiece with mapping to its color.
   * @param the_piece the piece to copy.
   * @return the copied piece to return
   */
  public AbstractPiece getCopyPiece(final AbstractPiece the_piece)
  {
    final int middle = my_width / 2 - 1; 
    AbstractPiece returnpiece = null;
    if (the_piece == null)
    {
      //Do nothing there is nothing to copy
      returnpiece = the_piece;
    }
    else if (the_piece.getColor() == Color.RED)
    {
      returnpiece = new SPiece(middle, STARTING_HEIGHT);
      returnpiece.setColor(Color.RED);
    }
    
    else if (the_piece.getColor() == Color.ORANGE)
    {
      returnpiece = new LPiece(middle, STARTING_HEIGHT);
      returnpiece.setColor(Color.ORANGE);
    }
    else if (the_piece.getColor() == Color.YELLOW)
    {
      returnpiece = new OPiece(middle, STARTING_HEIGHT);

      returnpiece.setColor(Color.YELLOW);
    }
    else if (the_piece.getColor() == Color.GREEN)
    {
      returnpiece = new JPiece(middle, STARTING_HEIGHT);
      returnpiece.setColor(Color.GREEN);
    }
    else if (the_piece.getColor() == Color.BLUE)
    {
      returnpiece = new ZPiece(middle, STARTING_HEIGHT);
      returnpiece.setColor(Color.BLUE);
    }
    else if (the_piece.getColor() == Color.CYAN)
    {
      returnpiece = new TPiece(middle, STARTING_HEIGHT);
      returnpiece.setColor(Color.CYAN);
    } 
    else if (the_piece.getColor() == Color.MAGENTA)
    {
      returnpiece = new IPiece(middle, STARTING_HEIGHT);
      returnpiece.setColor(Color.MAGENTA);
    }
   
    return returnpiece;
  
  }
  
  /**
   * Sets the mode of the pieces to be random or sequential.
   * @param the_setting whether it should be random (true), or not which is false
   */
  public final void setRandom(final boolean the_setting)
  {
    my_random = the_setting;
  }
    
  /**
   * Moves the Tetris piece left.
   */
  public void moveLeft()
  {
    if (canMoveLeft())
    {
      my_current_piece.moveLeft();
      setChanged();  
      notifyObservers();
    }
  }
  /**
   * Move the Tetris piece right.
   */
  public void moveRight()
  { 
    if (canMoveRight())
    {
      my_current_piece.moveRight();
      setChanged(); 
      notifyObservers();
    }
  }
  
  /**
   * Move the Tetris piece down.
   */
  public void moveDown()
  {
    if (canMoveDown())
    {
      my_current_piece.moveDown();
      setChanged(); 
      notifyObservers();
    }
  }
  
  /**
   * Rotates the tetris piece.
   */
  public void rotate()
  {
    if (canRotate())
    {
      my_current_piece.rotate();
      setChanged(); 
      notifyObservers();
    }
    
  }
  
  /**
   * Updates the tetris game by one step.  Calling this causes the tetris piece
   * to move down one row and update the board as required. 
   */
  public void step()
  {
    
    if (canMoveDown())
    {
      moveDown();
    }
     
    else 
    {

      freezePiece(true);
      final int maxheight = MAX_ROTATION - 1;
      final int location = my_current_piece.getY() - maxheight; 
      int counter = 0;

      for (int j = 0; j <= maxheight; j++)
      {
        if (location + j == -MAX_ROTATION)
        {
          setGameOver(true);
          break;
        }
        if (isLineFilled(location + j))
        {
          clearLines(location + j);
          my_number_of_lines_cleared++;
          counter++;
        }
      } 
      
      if (!my_game_over_status)
      {
        my_score = (int) (my_score + Math.pow(BASE_SCORE, counter));
        createPiece();
      }
      
    }
  }
  
  /**
   * The method moves the tetris piece as far as it can move down.  For example
   * if the board is empty the piece will move to the bottom of the board. 
   */
  public void dropDown()
  {
    while (canMoveDown())
    {
      step();
    }
    step(); //Comment out this if you want the piece not freeze right away when dropped
  }

  /**
   * This method empty the board.
   */
  public void clearAllLines()
  {
    for (int j = 0; j < my_height; j++)
    { 
      clearLines(j);
    }
    createPiece();
  }

  /**
   * Freezes the Piece to the board.
   * @param the_option set true to freeze piece or false to un-freeze
   */
  public void freezePiece(final boolean the_option)
  {
    for (int i = 0; i < MAX_ROTATION; i++)
    {  
      final int[] coordinates = getTrueCoordinates(i);
      final int x = coordinates[0];
      final int y = coordinates[1];
      if (isInBounds(x, y))
      {
        if (the_option) 
        {
          my_container[y][x] = my_current_piece.getColor();
          my_piece_frozen = true;
        }
        else
        {
          my_container[y][x] = null;
          my_piece_frozen = false; 
        } 
      }
    }   
  }

  /**
   * This method sets the game to be over.
   * @param the_state true is the game is to be over, false if the game should not be over.
   */
  public void setGameOver(final boolean the_state)
  {
    setChanged();
    notifyObservers();
    my_game_over_status = the_state;
  }
  
  /**
   * Returns the status of the game wether the game if over or not.
   * @return true if the game is over false otherwise.
   */
  public boolean isGameOver()
  {
    return my_game_over_status;
  }
  /**
   * Gets the values stored in the boar or container.
   * @return my_container the board with its present contents
   */
  public Color[][] getContainer()
  {
    final Color[][] copy_board = new Color[my_height][my_width]; 
    for (int i = 0; i < my_container.length; i++)
    {
      copy_board[i] = my_container[i].clone();
    }
    return copy_board;
  }
  
  /**
   * Provides a copy of the container provided.
   * @param the_container the container to copy
   * @return the copied container
   */
  private Color[][] getContainer(final Color[][] the_container)
  {
    final Color[][] copy_board = new Color[my_height][my_width]; 
    for (int i = 0; i < the_container.length; i++)
    {
      copy_board[i] = the_container[i].clone();
    }
    return copy_board;
  }
  
  /**
   * Sets the score of the game.
   * @param the_score the score to set the score of the game to.
   */
  public void setScore(final int the_score)
  {
    my_score = the_score;
  }
  
  /**
   * Gets the score of the game.
   * @return the current score of the game
   */
  public int getScore()
  {
    return my_score;
  }
  
  /**
   * Sets the number of lines cleared in the game.
   * @param the_number the number of lines cleared
   */
  public void setNumberOfLinesCleared(final int the_number)
  {
    my_number_of_lines_cleared = the_number;
  }
  
  /**
   * Gest the number of lines cleared in the game.
   * @return the number of lines cleared in the game.
   */
  public int getNumberOfLinesCleared()
  {
    return my_number_of_lines_cleared;
  }
  
  /**
   * Determines is a piece can move right.
   * @return true if the piece can move right
   */
  private boolean canMoveRight()
  {
    boolean move = true;
    int count = 0;
    for (int i = 0; i < MAX_ROTATION; i++)
    {
      final int[] coordinates = getTrueCoordinates(i);
      final int x = coordinates[0] + 1;
      final int y = coordinates[1];
      if (isInBounds(x, y))
      {
        if (!isCellEmpty(x, y))
        {
          move = false;
          break;
        }
      }
      else if (isInBounds(x, 0))
      {
        count++;
      } 
      else
      {
        move = false;
        break;
      }
    }
    if (count == MAX_ROTATION)
    {
      move = true;
    }
    return move;
  }
  
  /**
   * Determines is a piece can move right.
   * @return true if the piece can move right
   */
  private boolean canMoveLeft()
  {
    
    boolean move = true;
    
    for (int i = 0; i < MAX_ROTATION; i++)
    {
      final int[] coordinates = getTrueCoordinates(i);
      final int x = coordinates[0] - 1;
      final int y = coordinates[1];
      if (isInBounds(x, y))
      {
        if (!isCellEmpty(x, y))
        {
          move = false;
          break;
        }
      }
      else if (my_current_piece.getY() >= 0 && isInBounds(x, 0))
      {
        break;
      } 
      else
      {
        move = false;
        break;
      }
    }
    return move;

  } 
  /**
   * Determines if a tetris piece can move down.
   * @return true if the tetris piece can move down false otherwise
   */
  private boolean canMoveDown()
  {
    boolean move = true;
    
    for (int i = 0; i < MAX_ROTATION; i++)
    {
      final int[] coordinates = getTrueCoordinates(i);
      final int x = coordinates[0];
      final int y = coordinates[1] + 1;
      if (isInBounds(x, y))
      {
        if (!isCellEmpty(x, y))
        {
          move = false;
          break;
        }
      }
      else if (y < 0) // If its above the board
      {
        break;
      }
      else
      {
        move = false;
        break;
      }
    }
    return move;
    
  }
  
  /**
   * Determines if a tetris Piece can be rotated.
   * @return true if a piece can be rotated.
   */
  private boolean canRotate()
  { 
    boolean move = true;
    my_current_piece.rotate();
    for (int i = 0; i < MAX_ROTATION; i++)
    {
      final int[] coordinates = getTrueCoordinates(i);
      final int x = coordinates[0];
      final int y = coordinates[1];
          
      if (isInBounds(x, y))
      { 
        if (!isCellEmpty(x, y))
        {
          move = false;
          break;
        }
      }
      else if (!isInBounds(x, 0))
      {
        move = false;
        break; 
      }
    }
    my_current_piece.rotate();
    my_current_piece.rotate();
    my_current_piece.rotate();
    
    return move;
  }
  /**
   * Clears the given line on the board.
   * @param the_y the line to be cleared 
   */
  private void clearLines(final int the_y)
  {
    if (isInBounds(0, the_y))
    {
      System.arraycopy(getContainer() , 0, my_container, 1, the_y);
      for (int i = 0; i < my_width; i++)
      {  
        my_container[0][i] = null;
      }
     
      setChanged(); 
      notifyObservers();
    }
  }
  
  /**
   * Returns the true coordinate with respect to the board coordinates.
   * @param the_cell the x-coordinate of the cell block on a piece
   * @return returns the true x and y coordinate of the cell
   */
  private int[] getTrueCoordinates(final int the_cell)
  {
    final int[] real_coord = new int[2];
    final int [][] rotation = my_current_piece.getRotation();
    real_coord[0] = rotation[the_cell][0]  + my_current_piece.getX();
    real_coord[1] = rotation[the_cell][1] * -1 + my_current_piece.getY();
    
    return real_coord;
  }
  
  /**
   * Checks to see if the cell in the board.
   * @param the_x the x-coordinate of the cell
   * @param the_y teh y-coordinate of the cell
   * @return true if it is in bounds or in the board, false otherwise
   */
  private boolean isInBounds(final int the_x, final int the_y)
  {
    boolean result = false;
    if (the_x >= 0 && the_x < my_width && the_y < my_height && the_y >= 0)
    {
      result = true;
    }
    
    return result;
  }
  
  /**
   * Determines if the cell is empty.
   * @param the_x the x coordinate of the cell to check
   * @param the_y the y_coordinate of the cell to check
   * @return true if the cell is empty, false otherwise
   */
  private boolean isCellEmpty(final int the_x, final int the_y)
  {
    boolean result = false; 
    if (isInBounds(the_x, the_y) && my_container[the_y][the_x] == null)
    {   
      result = true;
    }
    return result;
  }
  
  /**
   * Checks to see if a particular line if filled on the board.
   * @param the_y the y coordinate of the line to check
   * @return true if the line given is filled
   */
  private boolean isLineFilled(final int the_y)
  {
    boolean result = true;

    for (int i = 0; i < my_width; i++)
    {
      if (isCellEmpty(i, the_y))
      {
        result = false;
        break;
      }
    }

    return result; 
  }
  @Override
  public String toString()
  {
    final StringBuilder sb = new StringBuilder();
    boolean moving = true;
    if (!my_piece_frozen)
    {
      freezePiece(true);
      moving = false;
    }
    
    final Color[][] copy = getContainer(); 

    for (int i = 0; i < my_height; i++)
    {
      sb.append("|");
      for (int j = 0; j < my_width; j++)
      {
       
        if (copy[i][j] == null)
        {
          sb.append("  ");
        }
        else
        {
          sb.append("[]");
          
        }
      }
     
      sb.append("|\n");
    }
    for (int i = 0; i <= my_width; i++)
    {
      sb.append("||");
    }
    
    freezePiece(moving);
    return sb.toString();
    
  }

  /**
   * Returns the list of the state of the board when a piece was created.
   * @return the list of 2D array of the board representation when a piece was created.
   */
  public List<Color[][]> getListOfContainers()
  {
    return my_container_list;
  }
  
}